Rate 是一個評分元件。一方面可以對於評價的數據展示,另一方面可以讓人進行對評分的操作。
因為 MUI 目前的版本還沒有 Rate 元件,因此我們這邊先只參考 Antd 的元件。
count
雖然我們平常常見的評分都是五顆星星,但這邊給了這個 count 的參數讓我們不被限制於只能五顆星星,我覺得還蠻不錯的,這邊的預設也是五顆星。
allowHalf
這個參數允許我們選擇半顆星星,而且他很酷的是,他允許我們只 hover 一半的星星,到底是怎麼做到的呢?這邊來看一下他的 html 結構:

另外,我們這邊發現 ant-rate-star-first 這個 div 節點有一個關鍵的 css 屬性,就是 width: 50%; ,如下圖所示:

透過上述的結構,我們可以大概猜出實作 hover 一半星星的邏輯,首先我們要準備 active 星星全顆,還有 inactive 星星全顆。
接著,在初始狀態,我們先把 width: 50%; 的 active 星星完全重疊在 inactive 星星上面(就是圖中的 star-first div),這邊是透過 position: absolute; 屬性來實作重疊的效果,並且讓 active 星星先隱藏,這邊隱藏的方式是把 opacity 變成 0。
當我們 hover 在左半邊的 star-first div 上面時,就讓 active 的星星顯現(opacity: 1;),並且保持 width: 50%;,這樣看起來就是半顆星 active 的效果;當我們 hover 在右半邊的 star-second div 上面時,我們就改變被疊在下面的星星的顏色,從灰色變成黃色,也就是從 inactive 變成 active,這樣看起來就會變成一顆全星的效果。
disabled
設定了 disabled 為 true 時, Rate 就不能夠讓使用者操作,變成已讀的狀態,也就是純展示的功能。
character
character 屬性也是讓我蠻驚艷的屬性之一,就是他做到可以替換 Rate 字符,言外之意就是你不一定要被他限制住是星星圖案,你也可以是愛心,甚至也可以是文字,我覺得這個非常的酷,因為上面我們解析他的半顆 hover 功能是透過改變 color 來實現,感覺傳入的屬性也是需要能夠支援 color 可以被改變,因此這邊文件寫說限定的型別就是 ReactNode,從範例中也可以看出,他支援 icon 以及文字的傳入。
| 屬性 | 說明 | 類型 | 默認值 |
|---|---|---|---|
| count | star 總數 | number | 5 |
| allowHalf | 是否允許半顆星星 | boolean | false |
| disabled | 是否能進行交互 | boolean | false |
| defaultValue | 預設分數 | number | |
| themeColor | 主題顏色 | number | |
| size | star 大小 | number | 32 |
| character | 自定義字符 | ReactNode, String |
簡化來看的話,我們 Rate 整體邏輯架構概念回如下,由一個 <RateWrapper /> 的根節點包覆住整個元件,並且也由這個跟節點決定內部元件佈局排版,以 Rate 為例,應該是 row 方向的佈局,因此我們可以用 flex 來實現。
我們決定 star character 總數的 props 是 count,因此由下面程式碼範例,我們傳入的 count 為多少,就能夠產生長度為多少的陣列:
<RateWrapper>
{
[...Array(count).keys()].map((itemKey) => (
<Character key={itemKey} />
))
}
</RateWrapper>

接著我們來實現 Character,我們預設的 Character 是星星 <StarIcon />,由先前元件分析可知,要做到能夠允許選取半顆星星,需要兩個元件 <CharacterFirst /> 和 <CharacterSecond /> 一起來搭配才能實現:
<RateWrapper>
{
[...Array(count).keys()].map((itemKey) => (
<CharacterWrapper key={itemKey}>
<CharacterFirst>{character}</CharacterFirst>
<CharacterSecond>{character}</CharacterSecond>
</CharacterWrapper>
))
}
</RateWrapper>
佈局上,為了實現半星選取,<CharacterFirst /> 必須要設為 position: absolute;,如此 <CharacterFirst /> 和 <CharacterSecond /> 才能夠重疊,並且 <CharacterFirst /> 的 width 需要設為 50% 來表示半星。
當我們不需要伴星選取的時候,只需要隱藏 <CharacterFirst /> 就能夠做到了。
const CharacterFirst = styled.div`
position: absolute;
color: ${(props) => (props.$isActive ? props.$starColor : '#F0F0F0')};
width: 50%;
overflow: hidden;
cursor: pointer;
`;
佈局完成之後,接著我們要做的是 hover 的時候能夠預覽選取樣式,因此 hover 到哪裡就要 active 到哪裡,當滑鼠移開的時候,則回覆到原本選取狀態

因此我們需要一個 state 用來記錄預覽狀態,另一個 state 則是用來記錄實際上的選取狀態:
const [innerValue, setInnerValue] = useState(defaultValue);
const [previewValue, setPreviewValue] = useState(innerValue);
當滑鼠 hover 上去的時候,我們呼叫 onMouseOver 事件,若 hover 在 <CharacterFirst /> 表示半星,所以要 +0.5;若 hover 在 <CharacterSecond /> 表示全顆星,所以要 +1。
<CharacterWrapper key={itemKey}>
<CharacterFirst
className="rate__character-first"
$starColor={starColor}
$isActive={itemKey + 0.5 <= previewValue}
onMouseOver={() => handleChangePreviewValue(itemKey + 0.5)}
onMouseLeave={() => handleChangePreviewValue(innerValue)}
onClick={() => handleOnClick(itemKey + 0.5)}
>
{character}
</CharacterFirst>
<CharacterSecond
className="rate__character-second"
$starColor={starColor}
$isActive={itemKey + 1 <= previewValue}
onMouseOver={() => handleChangePreviewValue(itemKey + 1)}
onMouseLeave={() => handleChangePreviewValue(innerValue)}
onClick={() => handleOnClick(itemKey + 1)}
>
{character}
</CharacterSecond>
</CharacterWrapper>
當滑鼠移開的時候,則透過 onMouseLeave 事件來改變 previewValue,設定回原本該有的值:
const handleChangePreviewValue = (currentValue) => {
if (!isDisabled) {
setPreviewValue(currentValue);
}
};
onClick 事件則是確定選取的時候呼叫,因此要改變 innerValue ,那如果 onClick 的時候我們發現選取值與原本的值一樣,則表示他想要取消選擇,此時我們將 innerValue 設為 0:
const handleOnClick = (clickedValue) => {
if (isDisabled) return;
setInnerValue((previousValue) => (previousValue === clickedValue ? 0 : clickedValue));
};
那到目前為止,關於 Rate 主要的關鍵功能就都完成了!透過以上的方法,我們藉由 props 來改變 character 也不會是難事了:

Rate 元件原始碼:
Source code
Storybook:
Rate